/**
 * JSLexer.js
 * Analisador léxico para JavaScript
 *
 * Andrei de A. Formiga, 2008-06-24
 *
 */

//
// NAO IMPLEMENTADO:
//  - literais de expressão regular
//  - inferência de ponto-e-vírgula (lexer ou parser?)
//


// Espaço em branco
var whiteSpace = /\s*/g;

var lineTerm = /[\n\r\u2028\u2029]/g;

// Comentários
var slComment = /\/\/.*$/mg;
var mlCommentStart = /\/\*/g;

// Identificadores
var id = /[a-zA-Z_\$](\w|\$)*/g;

// Literais numéricos
var fpLiteral = /\d*(\.\d*)?([Ee]([+-])?\d*)?/g;
var hexLiteral = /0[Xx][0-9A-Fa-f]*/g;
var octLiteral = /0[0-7]*/g;

// Literal string
var strLiteral = /('[^']*')|("[^"]*")/g; //'

// Pontuação
var nonUnitPunct =
  /<=|>=|==|!=|===|!==|\+\+|--|<<|>>|>>>|&&|\|\||\+=|-=|\*=|%=|<<=|>>=|>>>=|&=|\|=|\^=/g;
var unitPunctuator = /[{}\(\)\[\]\.;,<>\+\-\*%&\|\^!~\?:=]/g;
var divPunctuator = /\/|\/=/g;

// Palavras reservadas
var reservedWords = ["break", "case", "catch", "continue", "default", "delete",
		     "do", "else", "finally", "for", "function", "if", "in",
		     "instanceof", "new", "return", "switch", "this", "throw",
		     "try", "typeof", "var", "void", "while", "with",
		     "null", "true", "false"];   // literais reservados

// Palavras reservadas para o futuro
var futureReservedWords = ["abstract", "boolean", "byte", "char", "class",
			   "const", "debugger", "double", "enum", "export",
			   "extends", "final", "float", "goto", "implements",
			   "import", "int", "interface", "long", "native",
			   "package", "private", "protected", "public", "short",
			   "static", "super", "synchronized", "throws",
			   "transient", "volatile"];


// tipos de tokens que podem ser retornados
var tokenrexps = [id, strLiteral, unitPunctuator, nonUnitPunct, divPunctuator,
		  hexLiteral, octLiteral, fpLiteral];

// funcoes que processam matches em tokens retornaveis
var tokenprocs = [idProc, strProc, punctProc, punctProc, punctProc,
		  hexNumProc, octNumProc, numProc];


//--- Classe JSLexer ---
function JSLexer(source)
{
  this.source = source;
  this.pos = 0;
  this.lastMatch = "";
}

JSLexer.prototype.getNextToken =
  function()
  {
    var result = null;

    // Elimina espaços em branco e comentários
    while (this.match(whiteSpace) || this.match(slComment) ||
	   this.match(mlCommentStart))
    {
      if (this.lastMatch.match(mlCommentStart))
	this.pos = skipMLComment(this.source, this.pos);
    }

    // Verifica se chegou ao final da entrada
    if (this.pos >= this.source.length)
      result = Token.eof;
    else
    {
      for (var i = 0; i < tokenrexps.length; ++i)
      {
	if (this.match(tokenrexps[i]))
	{
	  result = tokenprocs[i](this.lastMatch);
	  break;
	}
      }
    }

    return result;
  };

JSLexer.prototype.match =
  function(re)
  {
    var src = this.source;
    var pos = this.pos;

    if (pos >= src.length)
      return false;

    re.lastIndex = pos;
    var ms = re.exec(src);
    var result = false;

    if (ms)
    {
      result = (ms[0] == src.substring(pos, re.lastIndex)) && re.lastIndex > pos;
      if (result)
      {
	this.pos = re.lastIndex;
	this.lastMatch = ms[0];
      }
    }

    return result;
  };



//--- Funcoes auxiliares ---

function isReservedWord(word)
{
  var i;

  for (i = 0; i < reservedWords.length; ++i)
    if (word == reservedWords[i])
      return true;

  for (i = 0; i < futureReservedWords.length; ++i)
    if (word == futureReservedWords[i])
      return true;

  return false;
}

// Funcoes que processam tokens
function idProc(str)
{
  if (isReservedWord(str))
    return new Token(Token.KEYWORD, str);
  else
    return new Token(Token.ID, str);
}

function numProc(str)
{
  return new Token(Token.NUMBER, parseFloat(str));
}

function hexNumProc(str)
{
  return new Token(Token.NUMBER, parseInt(str, 16));
}

function octNumProc(str)
{
  return new Token(Token.NUMBER, parseInt(str, 8));
}

function strProc(str)
{
  return new Token(Token.STRING, str.substring(1, str.length - 1));
}

function punctProc(str)
{
  return new Token(Token.PUNCT, str);
}


// Testa se a string source, a partir do indice index,
// é um match para a ER re. Um match ocorre apenas quando
// ele é encontrado na string logo a partir de index, não depois.
function match(source, index, re)
{
  re.lastIndex = index;
  var ms = re.exec(source);
  var result = false;

  if (ms)
  {
    result = (ms[0] == source.substring(index, re.lastIndex)) &&
	       re.lastIndex > index;
  }

  return result;
}

function findMLComment(source, index)
{
  mlCommentStart.lastIndex = index;
  if (mlCommentStart.test(source))
    return mlCommentStart.lastIndex;

  return -1;
}

function skipMLComment(source, index)
{
  var curIndex = index;
  var done = false;
  var result = index;

  while (!done)
  {
    // Verifica final do comentário
    if (source[curIndex] == "*" && source[curIndex + 1] == "/")
    {
      result = curIndex + 2;
      done = true;
    }
    else if (curIndex + 1 < source.length)
      curIndex++;
    else
      throw new Error("Fechamento de comentário esperado no fim da string");
  }

  return result;
}


// Versao complicada usando expressoes regulares
function skipMultiLineComment(source, index)
{
  var asterisks = /(\*)*/g;
  var nonAsterisks = /[^\*]*/g;
  var done = false;
  var nextIndex = index;

  while (!done)
  {
    // Le todos os caracteres que não são asteriscos
    nonAsterisks.lastIndex = nextIndex;
    if (nonAsterisks.exec(source))
      asterisks.lastIndex = nonAsterisks.lastIndex;
    else
      asterisks.lastIndex = index;

    // Le os asteriscos
    if (!asterisks.exec(source))
      throw new Error("Fechamento de comentário esperado no fim do arquivo");

    nextIndex = asterisks.lastIndex;
    if (source[nextIndex] == "/")    // fim de comentario encontrado
    {
      done = true;
      nextIndex++;
    }
  }

  return nextIndex;
}


